﻿#region 참조 네임스페이스

using System;
using System.IO;
using System.Reflection;
using System.Text;
using System.Diagnostics;
using Skying.Diagnostics.Logger.Coodinator;

#endregion

namespace Skying.Diagnostics.Logger.Entry
{
	/// <summary>
	/// <see cref="Skying.Diagnostics.Logger.Logger"/>를 통해 로깅할 때 파일을 통해 로그를 출력하는 <see cref="Skying.Diagnostics.Logger.Entry.ILogEntry"/>의 구현체를 정의한다.
	/// </summary>
	class FileLogEntry : LogEntry
	{
		#region 상수

		/// <summary>
		/// 로그 기본 디렉토리 명칭 상수를 나타낸다.
		/// </summary>
		/// <remarks>
		/// <para>로그 파일명이 별도의 경로 없이 설정된 경우 서브 디렉토리의 명칭으로 사용된다.</para>
		/// <para>로그 파일명이 서브 디렉토리를 포함하거나 절대 경로되어 있는 경우에는 사용되지 않는다.</para>
		/// </remarks>
		private const string _DEFAULT_LOG_DIRECTORY = "log";

		/// <summary>
		/// 로그 파일을 분류하기 위한 날짜 기준에 대한 포맷 상수를 나타낸다.
		/// </summary>
		private const string _DEFALUT_DATETIME_FORMAT = "yyyyMMdd";

		#endregion

		#region 변수

		/// <summary>
		/// 로그 디렉토리 경로를 나타낸다.
		/// </summary>
		private string _logDirectory;

		/// <summary>
		/// 로그 파일명을 나타낸다.
		/// </summary>
		private string _fileName;

		#endregion

		#region 프로퍼티

		/// <summary>
		/// 로그 파일의 절대 경로를 가져온다.
		/// </summary>
		public string LogPath
		{
			get
			{
				return string.Format(@"{0}\{1}", this._logDirectory, this._fileName);
			}
		}

		/// <summary>
		/// 로그 파일을 특정 정렬 기준으로 분류하여 저장할 지 여부를 가져온다.
		/// </summary>
		/// <remarks>
		/// <para></para>
		/// </remarks>
		public bool IsRolling
		{
			get;
			private set;
		}

		/// <summary>
		/// 로그 파일의 저장 기준을 가져온다.
		/// </summary>
		public FileRollingCriterion RollingCriterion
		{
			get;
			private set;
		}

		/// <summary>
		/// 파일명에 포함되는 날짜/시간 형식(<see cref="System.DateTime"/>)에 대한 포맷 문자열을 가져온다.
		/// </summary>
		public string DateTimeFormat
		{
			get;
			private set;
		}

		/// <summary>
		/// <see cref="Skying.Diagnostics.Logger.Entry.FileRollingCriterion.FileSize"/>가 적용된 엔트리인 경우 한 개의 로그 파일의 최대 크기를 가져온다.
		/// </summary>
		/// <remarks>
		/// <para><see cref="Skying.Diagnostics.Logger.Entry.FileRollingCriterion.FileSize"/>가 적용된 경우에만 사용되며 그렇지 않은 경우 무시된다.</para>
		/// </remarks>
		public int FileMaximumSize
		{
			get;
			private set;
		}

		#endregion

		#region 생성자/소멸자

		public FileLogEntry(string entryName, LogCoodinator coodinator, string logPath = "", bool isRolling = true, FileRollingCriterion rollingCriterion = FileRollingCriterion.Date, string dateTimeFormat = "yyyy-MM-dd", int fileMaximumSize = int.MaxValue)
			: base(entryName, coodinator)
		{
			AssemblyName assemblyName = Assembly.GetEntryAssembly().GetName();

			// 실행 프로세스의 최상위 경로를 가져온다.
			string rootPath = Path.GetDirectoryName(assemblyName.CodeBase);

			if (string.IsNullOrEmpty(logPath))
			{
				this._fileName = string.Format("{0}.log", assemblyName.Name);

				this._logDirectory = new Uri(string.Format(@"{0}\{1}", rootPath, _DEFAULT_LOG_DIRECTORY)).LocalPath;
			}
			else
			{
				// 로그 경로가 절대 경로인 경우
				if (Path.IsPathRooted(logPath))
				{
					FileInfo fileInfo = new FileInfo(new Uri(logPath).LocalPath);

					this._logDirectory = fileInfo.DirectoryName;

					this._fileName = fileInfo.Name;
				}
				// 로그 경로가 상대 경로인 경우
				else
				{
					// 실행 프로세스의 디렉토리와 로그 상대 경로를 결합한다.
					string combinedPath = Path.Combine(rootPath, logPath);

					FileInfo fileInfo = new FileInfo(new Uri(combinedPath).LocalPath);

					this._logDirectory = fileInfo.DirectoryName;

					this._fileName = fileInfo.Name;
				}
			}

			// 로그 디렉토리가 존재하지 않는 경우 새로 만든다.
			if (!Directory.Exists(this._logDirectory))
			{
				Directory.CreateDirectory(this._logDirectory);
			}

			//// 로그 파일이 존재하지 않을 경우 새로 만든다.
			//if (!File.Exists(this.LogPath))
			//{
			//	File.Create(this.LogPath).Close();
			//}

			this.IsRolling = isRolling;

			this.RollingCriterion = rollingCriterion;

			this.DateTimeFormat = dateTimeFormat == null ? _DEFALUT_DATETIME_FORMAT : dateTimeFormat;

			this.FileMaximumSize = fileMaximumSize;
		}

		#endregion

		#region 로깅

		/// <summary>
		/// 로그 메시지를 로그 파일에 기록한다.
		/// </summary>
		/// <remarks>
		/// <para>로그 파일 저장 기준(<see cref="Skying.Diagnostics.Logger.Entry.FileLogEntry.RollingCriterion"/>)에 의해 백업 파일이 생성된다.</para>
		/// </remarks>
		/// <param name="message">로깅할 메시지를 설정한다.</param>
		/// <param name="logSeverity">로그 심각도를 설정한다.</param>
		/// <param name="logDateTime">로깅 일시를 설정한다.</param>
		/// <param name="stackFrame">로그 발생 지점의 <see cref="System.Diagnostics.StackFrame"/>을 설정한다.</param>
		/// <param name="exception">로깅할 <see cref="System.Exception"/> 정보를 설정한다. null을 허용한다.</param>
		public override void Log(string message, LogSeverity logSeverity, DateTime logDateTime, StackFrame stackFrame, Exception exception = null)
		{
			try
			{
				byte[] binary = Encoding.UTF8.GetBytes(base.Coodinator.Print(message, logSeverity, logDateTime, stackFrame));

				// 가장 최근의 로그 파일 정보를 가져온다.
				FileInfo fileInfo = new FileInfo(this.LogPath);

				bool isNewCreate = false;

				// 로그 파일의 저장 기준이 날짜이고, 파일이 생성된 날짜가 오늘 날짜보다 과거인 경우
				if (fileInfo.Exists &&
					this.RollingCriterion == FileRollingCriterion.Date && fileInfo.CreationTime < DateTime.Today)
				{
					File.Move(this.LogPath, string.Format(@"{0}\{1}.{2}{3}", this._logDirectory, fileInfo.Name.Replace(fileInfo.Extension, string.Empty), fileInfo.CreationTime.ToString(this.DateTimeFormat), fileInfo.Extension));

					isNewCreate = true;
				}
				// 로그 파일의 저장 기준이 파일 크기이고, 파일 크기와 로그 메시지의 크기를 합친 크기가 파일 제한 크기보다 큰 경우
				else if (fileInfo.Exists &&
					this.RollingCriterion == FileRollingCriterion.FileSize && fileInfo.Length + binary.Length > this.FileMaximumSize)
				{
					File.Move(this.LogPath, this.CreateBackupFileName(fileInfo));

					isNewCreate = true;
				}
				// 로그 파일의 저장 기준이 날짜와 파일 크기이고, 파일이 생성된 날짜가 오늘 날짜보다 과거이거나 파일 크기와 로그 메시지의 크기를 합친 크기가 파일 제한 크기보다 큰 경우
				else if (fileInfo.Exists &&
					this.RollingCriterion == FileRollingCriterion.DateAndFileSize &&
					(fileInfo.CreationTime < DateTime.Today ||
					fileInfo.Length + binary.Length > int.MaxValue))
				{
					File.Move(this.LogPath, this.CreateBackupFileName(fileInfo));

					isNewCreate = true;
				}

				// 로그 파일을 새로 만들어야 하는 경우
				// 다음 코드 구간은 NT 시스템의 터널링에 대비한 구간이다.
				if (isNewCreate)
				{
					// 새로운 파일의 이름을 GUID를 통해 unique 하게 만든다.
					string newFilePath = string.Format(@"{0}\{1}", this._logDirectory, Guid.NewGuid());

					// unique한 이름으로 새로운 파일을 만든다.
					File.Create(newFilePath).Close();

					// 새로 만든 파일의 생성일자를 저장한다.
					DateTime creationDateTime = File.GetCreationTime(newFilePath);

					// 새로 만든 파일을 원본 로그 파일 이름으로 변경한다. 이 때, NT 시스템의 터널링의 영향을 받게 된다. 위에서 이전 로그 파일의 이름을 변경했을 때 터널링이 발생한다.
					File.Move(newFilePath, this.LogPath);

					// 원본 로그 파일의 생성일자를 unique한 이름으로 만든 파일의 생성일자로 변경한다.
					File.SetCreationTime(this.LogPath, creationDateTime);
				}

				// 로그 파일을 새로 만들거나 이미 존재하는 경우 열어서 로그를 기록한다.
				using (FileStream stream = new FileStream(this.LogPath, File.Exists(this.LogPath) ? FileMode.Open : FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite))
				{
					// 로그를 맨 끝부터 기록하기 위해 스트림의 위치를 스트림의 끝으로 이동시킨다.
					stream.Seek(0, SeekOrigin.End);

					stream.Write(binary, 0, binary.Length);

					stream.Flush();
				}
			}
			catch (Exception error)
			{
				throw error;
			}
		}

		/// <summary>
		/// 로그 파일을 백업하기 위한 파일명을 만든다.
		/// </summary>
		/// <remarks>
		/// <para>로그 파일의 저장 기준이 <see cref="Skying.Diagnostics.Logger.Entry.FileRollingCriterion.FileSize"/>이거나 <see cref="Skying.Diagnostics.Logger.Entry.FileRollingCriterion.DateAndFileSize"/>인 경우, 사용된다.</para>
		/// <para>같은 날짜에 로그 파일이 2개 이상이 될 수 있으므로 넘버링을 해야하는데 이 메서드를 통해 백업 파일명의 넘버링을 할 수 있다.</para>
		/// </remarks>
		/// <param name="fileInfo">로그 파일 정보를 설정한다.</param>
		/// <returns>새로운 백업 파일명이 반환된다.</returns>
		private string CreateBackupFileName(FileInfo fileInfo)
		{
			string backupFileName = string.Empty;

			try
			{
				// 백업 파일의 넘버링 번호를 나타낸다.
				int index = 1;

				while (true)
				{
					backupFileName = string.Format(@"{0}\{1}.{2}.{3}{4}", this._logDirectory, fileInfo.Name.Replace(fileInfo.Extension, string.Empty), fileInfo.CreationTime.ToString(this.DateTimeFormat), index, fileInfo.Extension);

					if (!File.Exists(backupFileName)) break;

					index++;

					continue;
				}
			}
			catch (Exception error)
			{
				throw error;
			}

			return backupFileName;
		}

		#endregion
	}
}